<?php

/**
 * Object Relational Mapper.
 * The reason hard-coded get/set methods begin with _get and _set is because
 * the __call() overloaded method looks for calls to get.. and set..().
 * @package Nina
 * @author John Main <john@thedaddy.org>, Chris Corbyn
 */
abstract class Nina_Orm extends Nina_Orm_Base
{
	/**
	 * Registry instance
	 * @var Nina_Registry
	 */
	protected $reg;
	
	/**
	 * Logger instance
	 * @var Nina_Logger
	 */
	protected $logger;

	/**
	 * Maps fields to the constants which represent them.
	 * @var array
	 */
	protected $column_names = array();
	
	/**	
	 * A container for the values of each field in a record.
	 * @var array
	 */
	protected $fields = array();
	
	/**	
	 * Any joined records.
	 * @var array
	 */
	protected $joins = array();
	
	/**
	 * The database which is used to do any querying.
	 * @var Nina_Db
	 */
	protected $db;
	
	/**
	 * A collection of field names which have been changed.
	 * @var array
	 */
	protected $modified_fields = array();
	
	/**
	 * True if this record has since been deleted.
	 * @var boolean
	 */
	protected $deleted = false;
	
	/**
	 * Cached field names to minimize on the number of times reflection is used.
	 * @var array
	 */
	protected $field_names = array();
	
	/**
	 * Objects which have been linked via relationship.
	 * @var array
	 */
	protected $joined_fields = array();
	
	/**
	 * A copy of the table name to minimize on the number of times reflection is used.
	 * @var string
	 */
	protected $table_name;
	
	/**
	 * The name of the subclass.
	 * @var string
	 */
	protected $class_name = __CLASS__;

	//Optionals

	/**
	 * The column the primary key refers to.
	 * Defaults to "id" if nothing else specified.
	 * @var string
	 */
	protected $primaryKey = false;
	
	/**
	 * Any foreign key constraints.
	 * @var array
	 */
	protected $foreignKeys = array();
	
	/**
	 * Any conditional associations
	 * @var array
	 */
	protected $conditionalAssociations = array();
	
	/**
	 * Any class translations for had many through relationships.
	 * @var array
	 */
	protected $classTranslations = array();
	
	/**
	 * Define has many through relationships.
	 * @var array
	 */
	protected $hasManyThrough = array();

	/**
	 * Flag if we are to dependent destroy link tables
	 * @var boolean
	 */
	protected $dependent_destroy_link_tables = false;

	/**
	 * Flag if we are to auto join
	 * @var boolean
	 */
	protected $auto_join = false;
	
	/**
	 * Flag fields to validate presence
	 * @var array
	 */
	protected $validate_presence = array();
	
	/**
	 * Flag fields to validate as numbers
	 * @var array
	 */
	protected $validate_number = array();
	
	/**
	 * Flag fields to validate by length
	 * @var array
	 */
	protected $validate_length = array();
	
	/**
	 * Flag fields to validate uniqueness of
	 * @var array
	 */
	protected $validate_unique = array();
	
	/**
	 * List of fields that contained validation errors
	 * @var array
	 */
	protected $validation_errors = array('presence' => array(), 'number' => array(), 'length' => array(), 'unique' => array());

	/**
	 * Constructor.
	 * @param Nina_Db [$db=null] A database to use if getDb() is not overridden in the subclass.
	 * @throws Nina_Exception_Orm If mapping data is not valid.
	 */
	public function __construct(Nina_Db $db = null, $obj = null) {
		if ($db !== null) {
			$this->setDb($db);
		}
		
		$this->reg = Nina_Registry::getInstance();
		$this->logger = Nina_Logger::getInstance();

		$this->reflectionSetup($obj);
		$this->configureFields();
		$this->resetErrors();
	}
	
	/**
	 * Initialize the object using reflection.
	 * This is used internally and should not be tampered with, hence the private access.
	 * @throws Nina_Exception_Orm If the ORM layout is not written correctly.
	 */
	protected function reflectionSetup($obj = null) {
		if(!$obj)
			$obj = $this;

		if (!empty($this->field_names)) return;

		$class = get_class($obj);
		$this->setClass($class);

		$reflector = new ReflectionClass($this->getClass());
		$constants = $reflector->getConstants();

		if(isset($constants["TABLE_NAME"])) {
			$this->table_name = $constants["TABLE_NAME"];
			unset($constants["TABLE_NAME"]);
		}
		else {
			$tables = $this->getDb()->getTables();

			$table_name = Nina_Utils::classToTableName($this->getClass());

			if(in_array($table_name, $tables))
				$this->table_name = $table_name;
			else
				throw new Nina_Exception_Orm('Table name cannot be calculated for ' . $this->getClass() . ' and no ' . $this->getClass() . '::TABLE_NAME constant has been defined');	
		}
		
		$field_names = array();
		
		/*
		// Could be used for auto-detecting of column names but is very slow and locating columns by constants seems to be for winners
		$fields = $this->getDb()->getFields($this->table_name);

		foreach($fields as $field) {
			$value = $this->table_name . '.' . $field;
			$field_names[$field] = $value;
		}
		*/
		
		//We may sometimes define table names in the form 'table_name AS table_alias'
		//This ensures that we match against the alias if defined
		if($last_space = strrpos($this->table_name, ' '))
			$matching_table_name = substr($this->table_name, $last_space + 1);
		else
			$matching_table_name = $this->table_name;

		//Fields are only picked up if they are defined in relation to the table name/alias
		foreach ($constants as $key => $value) {
			if($key == 'DEPENDENT_DESTROY_LINK_TABLES')
				$this->setDependentDestroyLinkTables($value);
			if($key == 'AUTO_JOIN')
				$this->setAutoJoin($value);
			elseif(strpos($value, $matching_table_name) !== false) {
				$lkey = strtolower($key);
				$lvalue = strtolower($value);
				$field_names[$lkey] = $lvalue;
			}
		}
		
		//Selectable both ways round
		$this->field_names = $field_names;
		$this->column_names = array_flip($field_names);
	}
	
	/**
	 * Set up any defaults and read options set in the mapping.
	 * @throws Nina_Exception_Orm If the configuration is invalid
	 */
	protected function configureFields() {
		if (!$this->primaryKey) {
			if($this->hasField('ID'))
				$this->primaryKey = $this->getField('ID');
			else
				throw new Nina_Exception_Orm('Unable to determine primary key for ORM class ' . $this->getClass());
		}
	}
	
	/**
	 * Get the name of the subclass.
	 * @return string
	 */
	public function getClass() {
		return $this->class_name;
	}
	
	/**
	 * Set the class name of the subclass.
	 * @param string $class Class name
	 */
	protected function setClass($class) {
		$this->class_name = $class;
	}
	
	/**
	 * Get the database object to use for queries.
	 * @return Nina_Db
	 */
	public function getDb() {
		return $this->db;
	}
	
	/**
	 * Set the database object to use for queries.
	 * This only works if the subclass does not provide a database.
	 * @param Nina_Db $db DB object
	 * @param Nina_Db
	 */
	public function setDb(Nina_Db $db) {
		$this->db = $db;
	}
	
	/**
	 * Get a hash map of all field names linked to their values.
	 * @return array
	 */
	public function getFields() {
		return $this->fields;
	}
	
	/**
	 * Get a hash map of all field names linked to their values without the leading table name.
	 * @param boolean [$leading_table=false] Show leading table
	 * @return array
	 */
	public function toArray($leading_table = false) {
		$fields = array();
		
		foreach($this->fields as $column => $value) {
			if(!$leading_table)
				$column = str_replace($this->table_name.'.', '', $column);
			$fields[$column] = $value;
		}
		
		return $fields;
	}
	
	/**
	 * Get a list of all available fields in the given object.
	 * The list comes from reflection in the first instance.
	 * @return array
	 */
	public function getFieldNames($include_joins = false) {
		if($include_joins) {
			$field_names = $this->field_names;

			foreach($field_names as $field => $column) {
				if(isset($this->foreignKeys[$column])) {
					$foreign_class = $this->foreignKeys[$column];
					$field_names[$field] = new $foreign_class();
				}
			}
			
			return $field_names;
		}
		else
			return $this->field_names;
	}
	
	/**
	 * Get a list of all available columns in the given table.
	 * @return array
	 */
	public function getColumnNames() {
		return $this->column_names;
	}
	
	/**
	 * Get the name of the database table this ORM object represents.
	 * Reflection is used to find this in the first instance.
	 * @return string
	 */
	public function getTableName() {
		return $this->table_name;
	}
	
	/**
	 * Gets all foreign keys which have been mapped.
	 * @return array
	 */
	protected function getForeignKeys() {
		return $this->foreignKeys;
	}
	
	/**
	 * Gets all the conditional associations
	 * @return array
	 */
	protected function getConditionalAssociations() {
		return $this->conditionalAssociations;
	}
	
	/**
	 * Gets all class translations which have been mapped (for use in 'has many through' relationships)
	 * @return array
	 */
	protected function getClassTranslations() {
		return $this->classTranslations;
	}
	
	/**
	 * Gets all has many through relationships
	 * @return array
	 */
	protected function getHasManyThrough() {
		return $this->hasManyThrough;
	}
	
	/**
	 * Set the dependent destroy link tables flag
	 * @param boolean [$flag=true] The flag value
	 */
	protected function setDependentDestroyLinkTables($flag = true) {
		$this->dependent_destroy_link_tables = $flag;
	}
	
	/**
	 * Get the dependent destroy link tables flag
	 * @return boolean
	 */
	protected function getDependentDestroyLinkTables() {
		return $this->dependent_destroy_link_tables;
	}
	
	/**
	 * Set the auto join flag
	 * @param boolean [$flag=true] The flag value
	 */
	protected function setAutoJoin($flag = true) {
		$this->auto_join = $flag;
	}
	
	/**
	 * Get the dependent destroy link tables flag
	 * @return boolean
	 */
	protected function getAutoJoin() {
		return $this->auto_join;
	}

	/**
	 * Check if a given table field exists in this ORM system.
	 * @param string $field Field name (constant name)
	 * @return boolean
	 */
	public function hasField($field) {
		return isset($this->field_names[strtolower($field)]);
	}

	/**
	 * Check if a given column exists in this ORM system.
	 * @param string $columm Field name (column name)
	 * @return boolean
	 */
	public function hasColumn($column) {
		return isset($this->column_names[strtolower($column)]);
	}

	/**
	 * Return a field value (column name)
	 * @param string $field Field name (constant name)
	 * @return boolean
	 */
	public function getField($field) {
		if ($this->hasField($field))
			return $this->field_names[strtolower($field)];
	}
	
	/**
	 * Get the name of a constant based on its value.
	 * This ultimately was found by reflection during the setup.
	 * @param string $field Actual field name
	 * @return string
	 */
	protected function getConstant($field) {
		if ($this->hasColumn($field))
			return strtoupper($this->column_names[$field]);
	}
	
	/**
	 * Add a new related record to this record.
	 * The record will be saved when save() is called on the parent object.
	 * @param string $field Field name
	 * @param Nina_Orm $obj
	 */
	protected function setJoinedField($field, Nina_Orm $obj) {
		$this->joined_fields[$field] = $obj;
		$this->addModifiedField($field);
	}
	
	/**
	 * Get a list of all the relationships linked to this record.
	 * @return array
	 */
	protected function getJoinedFields() {
		return $this->joined_fields;
	}
	
	/**
	 * Remove a stored object from the list of relationships to free up some memory.
	 * @param string $field Field name
	 */
	protected function removeJoinedField($field) {
		unset($this->joined_fields[$field]);
	}
	
	/**
	 * Return the validation errors
	 * @return array
	 */
	public function errors() {
		return $this->validation_errors;
	}
	
	/**
	 * Return the validation errors in a human readable format
	 * @return array
	 */
	public function readableErrors() {
		$readable_errors = array();
		
		foreach($this->validation_errors['presence'] as $error) {
			$readable_errors[] = ucwords($this->getConstant($error)) . ' must be given';
		}
		
		foreach($this->validation_errors['number'] as $error) {
			$readable_errors[] = ucwords($this->getConstant($error)) . ' must be a number';
		}
		
		foreach($this->validation_errors['length'] as $error) {
			$readable_errors[] = ucwords($this->getConstant($error)) . ' is too short';
		}
		
		foreach($this->validation_errors['unique'] as $error) {
			$readable_errors[] = ucwords($this->getConstant($error)) . ' already exists';
		}
		
		return $readable_errors;
	}
	
	/**
	 * Reset the validation errors
	 */
	protected function resetErrors() {
		$this->errors = array();
	}
	
	
	/**
	 * Check if a record is valid
	 * @return boolean
	 */
	public function valid() {
		$this->resetErrors();
		$error_found = false;
		
		foreach($this->validate_presence as $validate) {
			if(!$this->doGet($validate)) {
				$this->validation_errors['presence'][] = $validate;
				$error_found = true;
			}
		}
		
		foreach($this->validate_number as $validate) {
			if($this->doGet($validate) && !is_numeric($this->doGet($validate))) {
				$this->validation_errors['number'][] = $validate;
				$error_found = true;
			}
		}
		
		foreach($this->validate_length as $validate => $length) {
			if($this->doGet($validate) && strlen($this->doGet($validate)) < $length) {
				$this->validation_errors['length'][] = $validate;
				$error_found = true;
			}
		}
		
		foreach($this->validate_unique as $validate) {
			$class = $this->getClass();
			$c = self::newCriteria();
			$c->add($validate, $this->doGet($validate));
			
			// A new record will have no primary key set yet
			if(!$this->isNew()) $c->add($this->getPkField(), $this->getPk(), Nina_Orm_Criteria::NOT_EQUAL);
			
			if (self::doSelectOne(new $class(), $c)) {
				$this->validation_errors['unique'][] = $validate;
				$error_found = true;
			}
		}
		
		return !$error_found;
	}
	
	/**
	 * Save any changes.
	 * This is needed if changes made are to be persistent.
	 * @param boolean [$replace=false] Do a replace call instead of a straight insert (if new)
	 */
	public function save($replace = false) {
		//Nothing to do if no changes were made
		if ($this->isModified()) {
			if($this->valid()) {
				$this->saveAndPurgeJoins();
				//If a primary key can be found the record already exists
				if (!$this->isNew())
					return $this->saveAsUpdate();
				else
					return $this->saveAsNew($replace);
			}
			else
				return false;
		}
	}
	
	/**
	 * Save all joined relations to the database and remove them from memory.
	 */
	protected function saveAndPurgeJoins() {
		foreach ($this->getJoinedFields() as $field => $obj) {
			$obj->save();
			$pk = $obj->getPk();
			$this->doSet($field, $pk);
			$this->removeJoinedField($field);
		}
	}
	
	/**
	 * Run an UPDATE query to make this already existing record persistent.
	 */
	protected function saveAsUpdate() {
		$set = array();
		foreach ($this->getModifiedFields() as $field) {
			$set[$field] = $this->fields[$field];
		}
		return self::updateByPk($this, $set, $this->getPk());
	}
	
	/**
	 * Run an INSERT query to make this new record persistent.
	 * @param boolean [$replace=false] Do a replace call instead of a straight insert (if new)
	 * @return boolean
	 */
	protected function saveAsNew($replace = false) {
		$class = $this->getClass();
	
		$insert = array();
		foreach ($this->getFields() as $field => $value) {
			$insert[$field] = $value;
		}

		if($replace)
			return self::doReplace($this, $insert);
		else
			return self::doInsert($this, $insert);
	}
	
	/**
	 * Mark this record as being deleted.
	 * Used internally only.
	 * @param boolean [$set=true] Deleted flag
	 */
	protected function setDeleted($set=true) {
		$this->deleted = (bool) $set;
	}
	
	/**
	 * Check if this record has been deleted.
	 * @return boolean
	 */
	public function isDeleted() {
		return $this->deleted;
	}
	
	/**
	 * Check if this is a new record.
	 * @return boolean
	 */
	public function isNew() {
		return !($this->getPk() > 0);
	}
	
	/**
	 * Get the field which provides the primary key.
	 * @return string
	 */
	public function getPkField() {
		return $this->primaryKey;
	}
	
	/**
	 * Get the value of the primary key.
	 * @return mixed
	 */
	public function getPk() {
		return $this->doGet($this->getPkField());
	}
	
	/**
	 * Set the Primary Key of this record.
	 * @param mixed $pk PK
	 */
	public function setPk($pk) {
		$this->doSet($this->getPkField(), $pk);
	}
	
	/**
	 * Check if changes have been made to this record since it was created.
	 * @return boolean
	 */
	public function isModified() {
		$mod = $this->getModifiedFields();
		return !empty($mod);
	}
	
	/**
	 * Add a field to the stack of fields which have been updated since creation.
	 * @param string $field Field name
	 */
	protected function addModifiedField($field) {
		$this->modified_fields[$field] = $field;
	}
	
	/**
	 * Overwrite the current list of modified fields with these ones.
	 * @param array $fields Fields
	 */
	protected function setModifiedFields(array $fields) {
		$this->modified_fields = $fields;
	}
	
	/**
	 * Get a list of modified fields.
	 * @return array
	 */
	public function getModifiedFields() {
		return $this->modified_fields;
	}
	
	/**
	 * Inject a field value into this ORM object.
	 * This should never be called externally, it's an implementation detail only; forget about it ;)
	 * @param string|Nina_Orm $field Field name or joined object
	 * @param mixed $value Value
	 */
	protected function injectField($field, $value) {
		if($value instanceof self)
			$this->joins[$field] = $value;
		else
			$this->fields[$field] = $value;
	}
	
	/**
	 * Get a record by field name
	 * @param string $field Field name
	 * @return mixed
	 */
	public function doGet($field) {
		if (isset($this->fields[$field])) {
			return $this->fields[$field];
		}
	}
	
	/**
	 * Set a record by field name and value.
	 * This is only used internally.
	 * @param string $field Field name
	 * @param mixed $value Value
	 */
	protected function doSet($field, $value) {
		if($this->hasColumn($field)) {
			$this->injectField($field, $value);
			$this->addModifiedField($field);
		}
	}
	
	/**
	 * Reload all of a model's fields from the database
	 * @return boolean success or fail
	 */
	public function reload() {
		$class = $this->getClass();
		if($reloaded = self::retrieveByPk(new $class(), $this->getPk())) {
			foreach ($reloaded->getFields() as $field => $value) {			
				$this->injectField($field, $value);
			}
			return true;
		}
		else return false;
	}
	
	/**
	 * Calls the set sequence function to move a record up (sequence-wise) in a table
	 * @param Nina_Orm_Criteria [$c=null] The search criteria
	 */
	public function moveUp($c = null) {
		return $this->doSequence(true, false, $c);
	}
	
	/**
	 * Calls the set sequence function to move a record down (sequence-wise) in a table
	 * @param Nina_Orm_Criteria [$c=null] The search criteria
	 */
	public function moveDown($c = null) {
		return $this->doSequence(false, false, $c);
	}
	
	/**
	 * Calls the set sequence function to mov a record up (sequence-wise) in a table
	 * @param Nina_Orm_Criteria [$c=null] The search criteria
	 */
	public function moveToTop($c = null) {
		return $this->doSequence(true, true, $c);
	}
	
	/**
	 * Calls the set sequence function to move a record down (sequence-wise) in a table
	 * @param Nina_Orm_Criteria [$c=null] The search criteria
	 */
	public function moveToBottom($c = null) {
		return $this->doSequence(false, true, $c);
	}
	
	/**
	 * Moves a record up or down
	 * @param boolean [$up=true] Move record up?
	 * @param boolean [$move_to_limit=false] Move to the top/bottom
	 * @param Nina_Orm_Criteria [$c=null] The search criteria
	 */
	protected function doSequence($up = true, $move_to_limit = false, $c) {
		if(!$c)
			$c = self::newCriteria();

		$class = $this->getClass();
		
		if($this->hasField('SEQUENCE')) {
			$sequence = $this->getSequence();

			if($up) {
				if($move_to_limit) {
					$c->addOrderBy($this->getField('SEQUENCE'), Nina_Orm_Criteria::ORDER_ASC);
				}
				else {
					$c->add($this->getField('SEQUENCE'), $sequence, Nina_Orm_Criteria::LESS_THAN);
					$c->addOrderBy($this->getField('SEQUENCE'), Nina_Orm_Criteria::ORDER_DESC);
				}
			}
			else {
				if($move_to_limit) {
					$c->addOrderBy($this->getField('SEQUENCE'), Nina_Orm_Criteria::ORDER_DESC);
				}
				else {
					$c->add($this->getField('SEQUENCE'), $sequence, Nina_Orm_Criteria::GREATER_THAN);
					$c->addOrderBy($this->getField('SEQUENCE'), Nina_Orm_Criteria::ORDER_ASC);
				}
			}

			if ($replace_record = self::doSelectOne(new $class(), $c)) {
				$new_sequence = $replace_record->getSequence();

				$replace_record->setSequence($sequence);
				$replace_record->save();

				$this->setSequence($new_sequence);
				$this->save();

				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * Called before the __call() overloaded method does anything internally.
	 * This may be overridden in a subclass to perform so additional logic.
	 * @param string $type Call type (as in get or set)
	 * @param string $name Field name (from the database, not OM)
	 * @param array $args Arguments
	 * @return mixed
	 */
	protected function preCall($type, $name, $args) {
	}
	
	/**
	 * Extract the class from a database field
	 * @param string $field The database field
	 * @return string
	 */
	protected function extractClass($field) {
		//Get the reference (constant) that applies to the given database field within the model
		$reference = $this->column_names[strtolower($field)];
		
		//Remove any '_ID' suffix
		$class = Nina_String::doCamelise(str_replace('_id', '', $reference));

		//Perform any necessary class translations
		$translations = $this->getClassTranslations();

		if(isset($translations[$class]))
			$class = $translations[$class];

		return $class;
	}
	
	/**
	 * Overloaded magic method.
	 * This handles all calls to get() and set() methods for the various fields.
	 * @param string $method Method name called
	 * @param array $args Argument list passed
	 * @return mixed
	 * @throws Nina_Exception_Orm If no such method exists
	 */
	public function __call($method, $args) {

		$opt = substr($method, 0, 3);
		if (!in_array($opt, array('get', 'set'))) {
			$error_message = 'Tried to call unknown method ' . $this->getClass() . '::' . $method;

			if($this->reg->get('logging.soft_fail'))
				$this->logger->error($error_message);
			else
				throw new Nina_Exception_Orm($error_message);
		}

		$original_field = substr($method, 3);

		//Perform any necessary class translations
		$translations = $this->getClassTranslations();

		if(isset($translations[$original_field]))
			$original_field = $translations[$original_field];

		$field = Nina_String::doUnderscore($original_field);

		//A subclass may want to do something else
		if ($ret = $this->preCall($opt, $field, $args))
			return $ret;

		//Look for a value matching this field
		foreach ($this->getFieldNames() as $orm_field => $actual_field) {
			if (strtolower($orm_field) == $field) {
				switch ($opt) {
					case "set":
						return $this->doSet($actual_field, $args[0]);
					case "get":
						return $this->doGet($actual_field);
				}
			}
		}

		foreach($this->getConditionalAssociations() as $alias => $data) {
			if($alias == $original_field) {
				list($class, $conditions) = $data;

				if(class_exists($class)) {
					$c = self::newCriteria();
					
					foreach($conditions as $field => $value) {
						if($this->hasColumn($value)) {
							$c->add($field, $this->doGet($value));
						}
						else
							$c->add($field, $value);
					}

					return self::doSelect(new $class(), $c);
				}
			}
		}

		//If we've done a select join and we're looking for a joined record then it should be in here
		if($opt == 'get' && isset($this->joins[$original_field])) {
			return $this->joins[$original_field];
		}

		$translations = $this->getClassTranslations();

		//Look for 'belongs to' relationship (i.e. foreign key is in this table)
		foreach ($this->getForeignKeys() as $actual_field => $orm_class) {

			$actual_field_class = $this->extractClass($actual_field);

			if(isset($translations[$actual_field_class]))
				$actual_field_class = $translations[$actual_field_class];
			
			if ($actual_field_class == $original_field) {
				switch ($opt) {
					case "set":
						if (!($args[0] instanceof $orm_class)) {
						
							if(is_string($args[0]) || is_numeric($args[0]))
								$args_class = $args[0];
							elseif(is_array($args[0]))
								$args_class = 'Array';
							elseif(is_null($args[0]))
								$args_class = 'NULL';
							elseif(in_array('getClass', get_class_methods($args[0])))
								$args_class = $args[0]->getClass();
							else
								$args_class = get_class($args[0]);

							$error_message = 'Cannot set joined record of type ' . $orm_class . ' because object passed is of type ' . $args_class;
						
							if($this->reg->get('logging.soft_fail'))
								$this->logger->error($error_message);
							else
								throw new Nina_Exception_Orm($error_message);
						}

						//Set the foreign key first so that validation knows about it
						$this->doSet($actual_field, $args[0]->getPk());
						//Set the joined field so that they can be saved together
						return $this->setJoinedField($actual_field, $args[0]);

					case "get":
						if (($fk = $this->doGet($actual_field))) {
							$deleted = (count($args) > 0) ? $args[0] : false;
							if(($model = self::retrieveByPk(new $orm_class(), $fk, $deleted)))
								return $model;
							else
								return new Nina_Orm_Dummy($orm_class);
						}

						return new Nina_Orm_Dummy($orm_class);
				}
			}
		}

		//Crossing the streams!!!
		//Only works on 'gets'
		if($opt == 'get') {
			if(count($this->getHasManyThrough())) {
				foreach ($this->getHasManyThrough() as $hm_alias => $class_details) {
					if(count($class_details) == 2) {

						$hm_class_name = $class_details[0];
						$through_class_name = $class_details[1];

						if($hm_alias == $original_field && class_exists($hm_class_name) && class_exists($through_class_name)) {

							$through_class = new $through_class_name();
							$deleted = (count($args) > 0) ? $args[0] : false;

							foreach ($through_class->getForeignKeys() as $actual_field => $orm_class) {

								if($orm_class == $this->getClass()) {

									$reference = $through_class->column_names[$actual_field];

									if($through_class->hasField($reference)) {
										$c = self::newCriteria();
										$c->add($through_class->getField($reference), $this->doGet($this->getPkField()));

										//If a sequence field exists in the through table, order by that by default
										if($through_class->hasField('SEQUENCE'))
											$c->addOrderBy($through_class->getField('SEQUENCE'), Nina_Orm_Criteria::ORDER_ASC);
										else
											$c->addOrderBy($through_class->getField('ID'), Nina_Orm_Criteria::ORDER_ASC);

										$throughs = self::doSelect($through_class, $c, true);
										$hms = array();
										
										$translations = $this->getClassTranslations();

										if(isset($translations[$hm_class_name]))
											$hm_class_name = $translations[$hm_class_name];

										$hm_method = 'get'.$hm_class_name;

										if($throughs) {
											foreach ($throughs as $through) {
												if($through->$hm_method($deleted))
													$hms[] = $through->$hm_method($deleted);
											}
										}

										return $hms;
									}
								}
							}		

							//If we don't get a match we want to return this to protect iterative methods
							return array();					
						}
					}
				}
			}

			if(!class_exists($original_field)) {
				$underscored = Nina_String::doUnderscore($original_field);

				if($position = strrpos($underscored, '_')+1) {
					$last_word = substr($underscored, $position);
					$original_field = Nina_String::doCamelize(str_replace($last_word, Nina_Inflect::singularize($last_word), $underscored));
				}
				else
					$original_field = ucfirst(Nina_Inflect::singularize($underscored));
			}

			//Look for 'has many' relationship (i.e. foreign key is in the other table)
			if(class_exists($original_field)) {
				$hm_class = new $original_field();

				foreach ($hm_class->getForeignKeys() as $actual_field => $orm_class) {

					if($orm_class == $this->getClass()) {
						$reference = $hm_class->column_names[$actual_field];

						if($hm_class->hasField($reference)) {
							#you can pass in a criteria object for the association if you want
							if(count($args) && get_class($args[0]) == 'Nina_Orm_Criteria')
								$c = $args[0];
							else {
								$c = self::newCriteria();

								#sorting can be specified using getThing('updated_at') or getThing('updated_at desc')
								if(count($args)) {
									$sort_data = explode(' ', strtoupper($args[0]));
									$sort = $sort_data[0];
	
									if(count($sort_data) > 1 && ($sort_data[1] == 'ASC' || $sort_data[1] == 'DESC')) $direction = $sort_data[1];
									else $direction = Nina_Orm_Criteria::ORDER_ASC;
	
									//Sort by given field by preference, otherwise by sequence if table has one, otherwise by ID
									if($hm_class->hasField($sort))
										$c->addOrderBy($hm_class->getField($sort), $direction);
									elseif($hm_class->hasField('SEQUENCE'))
										$c->addOrderBy($hm_class->getField('SEQUENCE'), $direction);
									else
										$c->addOrderBy($hm_class->getField('ID'), Nina_Orm_Criteria::ORDER_ASC);
								}
								else {
									if($hm_class->hasField('SEQUENCE'))
										$c->addOrderBy($hm_class->getField('SEQUENCE'), Nina_Orm_Criteria::ORDER_ASC);
									else
										$c->addOrderBy($hm_class->getField('ID'), Nina_Orm_Criteria::ORDER_ASC);
								}
							}
							
							//if there is another arg then we will have been passed a flag to say look for deleted records or not
							$deleted = (count($args) == 2) ? $args[1] : false;

							$c->add($hm_class->getField($reference), $this->doGet($this->getPkField()));

							return self::doSelect($hm_class, $c, $deleted);
						}
					}
				}
							
				//If we don't get a match we want to return this to protect iterative methods
				return array();
			}
		}

		//Nowhere left to go, bad method name
		$error_message = 'Tried to call unknown (overloaded) method ' . $this->getClass() . '::' . $method;

		if($this->reg->get('logging.soft_fail'))
			$this->logger->error($error_message);
		else
			throw new Nina_Exception_Orm($error_message);
	}
	
	/**
	 * Delete the record if it's saved in the database.
	 * The data remains available even though it's no longer persistent.
	 * @return boolean
	 */
	public function delete() {
		if (!$this->isNew()) {
			if(count($this->getHasManyThrough())) {
				foreach ($this->getHasManyThrough() as $hm_alias => $class_details) {
					if($this->getDependentDestroyLinkTables() || (count($class_details) > 2 && $class_details[2])) {
						if(count($class_details) >= 2) {
							$hm_class_name = $class_details[0];
							$through_class_name = $class_details[1];
							
							if(class_exists($hm_class_name) && class_exists($through_class_name)) {
								$through_class = new $through_class_name();

								foreach ($through_class->getForeignKeys() as $actual_field => $orm_class) {
									//Check for a class match
									if($orm_class == $this->getClass()) {
										//Check if this field exists
										if($through_class->hasColumn($actual_field)) {
											$c = self::newCriteria();
											$c->add($actual_field, $this->doGet($this->getPkField()));
											$throughs = self::doSelect($through_class, $c);

											if($throughs) {
												foreach ($throughs as $through) {
													$through->delete();
												}
											}
										}
									}
								}
							}
						}
					}
				}
			}
			
			return self::deleteByPk($this, $this->getPk());
		}

		return false;
	}
	
	/**
	 * Sets whether or not to show the deleted records
	 * This is only used internally.
	 * @param Nina_Orm_Criteria $c The criteria to add the deleted flag to
	 * @param boolean $show_deleted Show deleted records or not
	 */
	protected function setShowDeleted(Nina_Orm_Criteria $c, $show_deleted = false) {
		if(!$show_deleted && $this->hasField('DELETED_AT'))
			$c->add($this->getField('DELETED_AT'), null, Nina_Orm_Criteria::IS);
		
		return $c;
	}
	
	/**
	 * Sets domain specific flag for multi-application data
	 * This is only used internally
	 * @param Nina_Orm_Criteria $c The criteria to add the deleted flag to
	 */
	protected function setSelectDomain(Nina_Orm_Criteria $c) {
		if(defined('DOMAIN') && $this->hasField('DOMAIN')) {
			$domain_criteria = self::newCriteria();
			$domain_criteria->add($this->getField('DOMAIN'), DOMAIN);
			$domain_criteria->addOr($this->getField('DOMAIN'), null, Nina_Orm_Criteria::IS);
			$domain_criteria->addOr($this->getField('DOMAIN'), '');
			$c->add($domain_criteria);
		}
		
		return $c;
	}
	
	/**
	 * Sets domain specific flag for multi-application data
	 * @param array $fields The insert fields
	 */
	protected function setInsertDomain($fields) {
		if(defined('DOMAIN') && $this->hasField('DOMAIN')) {
			$this->setDomain(DOMAIN);
			$fields[$this->getField('DOMAIN')] = DOMAIN;
		}
		
		return $fields;
	}
	
	/**
	 * Make sure to reset the primary key when cloning
	 */
	public function _clone() {
		$this->setPk(0);
	}
}
